package com.db4o.odbgen.plugins.xmlschema;

import java.util.ArrayList;
import java.util.List;

import com.db4o.internal.odbgen.StringUtils;
import com.db4o.odbgen.OdbgenException;

/**
 * 
 * Represents an element in the XmlSchema.
 * This may be either an xml element or an xml attribute.
 * @author liviug
 *
 */
public class Element {
    private List<Element> _children = new ArrayList<Element>();
    private Element _parent;
    private String _name;
    private String _content;
    private boolean _isUsed;
    private String _filePath;
    
    public Element(
            String name, 
            Element parent, 
            List<Element> children, 
            String content
            ) throws OdbgenException{
        this._children = children;
        this._content = content;
        this._name = name;
        this._parent = parent;
        
    }
    
    public List<Element> getAllChildren(){
        this.setIsUsed(true);

        if(this._children == null){
            this._children = new ArrayList<Element>();
        }
        return this._children;
    }
    
    /**
     * Returns children given their name.
     * @param name The name to search children for.
     * @return
     */
    public List<Element> getChildren(String name) {
        this.setIsUsed(true);

        List<Element> res = new ArrayList<Element>();
        for(Element child : this.getAllChildren()){
            if(child.getName().equals(name)){
                res.add(child);
            }
        }
        return res;
    }
    
    public Element getParent(){
        this.setIsUsed(true);

        return this._parent;
    }

    /**
     * Returns a child element identified by name.
     * If no child is found or more children are found, 
     * this method throws an {@link OdbgenException}.
     * @param name The name of the child element to search for.
     * @return The unique child corresponding to the given name.
     */
    public Element getSingleChild(String name) throws OdbgenException {
        this.setIsUsed(true);

        Element res = null;
        int no = 0;
        for(Element child : this.getAllChildren()){
            if(child.getName().equals(name)){
                res = child;
                no++;
            }
        }
        
        //Exactly one element should have been found.
        if(no!=1){
            throw new OdbgenException("The element '%s' must have one single child named '%s'. '%d' have been found",
                    this.getPath(),
                    name,
                    no
                    );
        }
        
        return res;
    }
    
    /**
     * Returns true if this element contains exactly one child.
     * Otherwise returns false. 
     * @param name The name of child element to search for.
     */
    public boolean hasSingleChild(String name){
        this.setIsUsed(true);

        int no = 0;
        for(Element child : this.getAllChildren()){
            if(child.getName().equals(name)){
                no++;
            }
        }
        
        //Exactly one element should have been found.
        if(no==1)
            return true;
        else
            return false;
    }
    
    
    /**
     * Returns the path of this element.
     * @return
     */
    public String getPath() {
        this.setIsUsed(true);

        List<Element> path = new ArrayList<Element>();
        Element e = this;
        while(e!=null){
            path.add(e);
            e=e.getParent();
        }
        
        //Iterate in reverse way.
        StringBuilder res = new StringBuilder(); 
        for(int i = path.size()-1; i>=0; i--){
            res.append('/');
            res.append(path.get(i).getName());
        }
        
        return res.toString();
    }

    /**
     * Returns the value of a boolean child element.
     * If the element is not found, false is returned.
     * @throws OdbgenException If there are multiple elements with the same name; 
     */
    public boolean getBooleanChildValue(String elementName) throws OdbgenException{
        this.setIsUsed(true);

        if(this.hasSingleChild(elementName)){
            return Boolean.parseBoolean(this.getSingleChild(elementName).getContent());
        }
        else{
            if(this.getChildren(elementName).size()!=0){
                throw new OdbgenException(
                        "Could not calculate value of boolean xml schema element '%s'. There are two many children of the same name.", 
                        elementName
                        );
            }
            else{
                return false;
            }
        }
    }

    
    /**
     * The name of the xml element.
     */
    public String getName() {
        //Simply getting the name of the element will not mark it as being used.
        
        return _name;
    }

    /**
     * The string content of the element.
     * This is never null. If there is no content, this will be empty.
     * Only contents for xml attributes are consider. Contents of xml elements
     * are ignored and are reported as empty strings by this method.
     */
    public String getContent() {
        this.setIsUsed(true);

        if(this._content == null){
            this._content = "";
        }
        return this._content;
    }

    /**
     * This is used to mark the fact an element is used.
     * An element is marked as being used when it's content is queried.
     */
    private void setIsUsed(boolean value) {
        this._isUsed = value;
    }

    /**
     * This is used for statistical information.
     * It indicates whether an element was used during code generation.
     * @return
     */
    public boolean isUsed() {
        return _isUsed;
    }

    /**
     * Returns the path of the file that contains this element.
     * This is used for debug purposes.
     * If the file is not defined, returns 'Unknown file'.
     */
    public String getFilePath() {
        if(this._filePath==null){
            return "Unknown file";
        }
        else{
            return this._filePath;
        }
    }

    /**
     * Sets the path of the file that contains this element.
     * This is used for debug purposes.
     */
    public void setFilePath(String filePath) {
        this._filePath = filePath;
    }
    
    
    /**
     * Returns a string representing this object.
     * This is used for debug purposes.
     * @param tabs Number of tabs to indent.
     */
    String toString(int tabs) {
        boolean showPath = false;
        StringBuilder res = new StringBuilder();
        res.append(String.format("%s%s%s%s%s",
                StringUtils.tabs(tabs),
                this.getName(),
                this.getContent().length()==0?"":("="+this.getContent()),
                        showPath?"["+this.getPath()+"]":"",
                                StringUtils.newLine()
        ));
        for(Element child : this.getAllChildren()){
            res.append(child.toString(tabs+1));
        }
            
        return res.toString();
    }





}



